Skip to content

fix(ssh): SSH environments connect over links with about a second of round trip - #35418

Merged
juliusmarminge merged 2 commits into
mainfrom
factory/issue-7733-a7815
Nov 12, 2026
Merged

juliusmarminge merged 2 commits into
mainfrom
factory/issue-7733-a7815

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Each SSH tunnel readiness probe (an HTTP GET through the local ssh -L forward) was capped at SSH_READY_PROBE_TIMEOUT_MS = 1000ms. At about 1s RTT one request through the forward takes several round trips (3-4s measured), so every probe timed out inside the 20s budget and the environment never connected. The tunnel-side waitForHttpReady wrapper in packages/ssh/src/tunnel.ts now defaults to a new SSH_TUNNEL_READY_PROBE_TIMEOUT_MS = 5000. The overall budgets (20s first connect, 2s check of an existing tunnel) are unchanged, and a fast link still succeeds on its first probe. SSH_READY_PROBE_TIMEOUT_MS (1s) still drives the probe inside the remote launch script, which runs on the remote host and does not cross the link, and the launch-lock wait arithmetic. The shared httpReadiness default is untouched. Reproduced first with the repro branch test (fails with 'Backend readiness probe exceeded 1000ms'), which passes with the fix. No new regex or parser.

Fixes #7733

Reproduction on main

Commit f03db4f (origin/main). Built a real high-latency SSH host: a network namespace joined to the host by a veth pair, with tc netem adding 500ms each way on the veth ends only (ping RTT 1000ms), a dedicated sshd on 10.77.0.2:2222, a throwaway remote user with Node, and the published t3 0.1.9 linux-x64 release archive served from a mirror inside the namespace. A scratch driver ran the desktop's real SshEnvironmentManager.ensureEnvironment (NodeServices, NodeCrypto, NodeHttpClient.layerUndici, NetService, as the desktop main process wires it) with archiveVersion 0.1.9; the system ssh got its config through a PATH wrapper. Baseline with no added latency: connected, managed remote server, pairing token issued. At 1000ms RTT: failed after 56s with ssh.tunnel.ready.failed (processRunning: true, localPortListening: true) and SshReadinessError 'Timed out waiting 20000ms for backend readiness' caused by 'Backend readiness probe exceeded 1000ms' (19 attempts). The remote server stayed up and answered 200 locally in 2ms. curl through an identical manual ssh -L tunnel returned 200 in 4.0s, 3.0s and 3.0s. A unit test in packages/ssh/src/tunnel.test.ts (HTTP responses taking 2.5s through the tunnel) fails on main with the same timeout message, and passed with the per-probe cap raised to 5s (reverted).

Evidence

In each separately built Linux Electron desktop package, open Settings → Environments → Add environment → SSH, enter slow-link / t3latency7733 / port 2222, and click Add environment. Wait for the readiness error or the saved row to say Connected. Both runs used fresh synthetic userdata, the published remote t3 0.1.9 runtime, light theme, a 1100×780 window, and the same real OpenSSH host with 500 ms netem delay in each direction (measured RTT 1000.069 ms).

Before After
f03db4fbef 04ab91311c
Before: readiness timeout at 1000 ms RTT After: SSH environment Connected at the same RTT
Real-time recording — readiness error after 58.2 s Real-time recording — Connected after 95.9 s

UI driver · SSH host setup · Reproduction details · Runtime summary · Redacted original SSH events

Control: the healthy remote returned HTTP 200 locally in 0.001629 s and through a separate real SSH tunnel in 3.003298 s. Base diagnostics showed processRunning=true, localPortListening=true, and probes exceeding the 1000 ms cap. Both final stills were identical 500 ms apart; both recordings were inspected through their ending.

Remaining limitation: head's existing-tunnel reuse check still has a 2000 ms total deadline. It rejected the healthy tunnel and recreated it before the client reached Connected. This proves eventual opening at this RTT, not seamless tunnel reuse.

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • Seamless reconnect/tunnel reuse is not proved: head's existing-tunnel check retains a 2000 ms total deadline and recreated the healthy tunnel before reaching Connected.
  • Windows/macOS, other RTTs, actual in-flight Wi-Fi, and archive download/install over the slow link were not exercised; the remote runtime was preinstalled.

Tests

cd packages/ssh && vp test run src/tunnel.test.ts -t 'longer than a second': failed before the fix (probe exceeded 1000ms), passes after. vp test run src/ (packages/ssh): 45/46 pass; the one failure, 'concurrent launches on one remote home record the server that owns it' (5s test timeout), also fails on the base commit without this change and when run alone on this VM, and passed inside check:changed. vp run check:changed: format, lint, knip, typecheck (5 packages), tests (2 files, 25 tests) all pass.

Not in this change

  • The 2s budget for checking an existing tunnel (waitForHttpReady with timeoutMs 2000 in ensureTunnel) can still drop a healthy tunnel and rebuild it on a very slow link; it no longer fails outright, because one probe is cut off at the 2s overall timeout and the tunnel is rebuilt.
  • The test 'concurrent launches on one remote home record the server that owns it' times out at 5s on this VM, on the base commit too.

AI-assisted; verified with the tests and tools listed above.

Reproduces #7733: each readiness probe through the SSH forward is capped at
1000ms, so a high-RTT link never connects even though the server is healthy.

AI-assisted; verified with the tests and tools listed above.
…round trip

Fixes #7733

AI-assisted; verified with the tests and tools listed above.
@github-actions github-actions Bot added vouch:trusted Author is trusted by repo permissions or the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Nov 12, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — SSH tunnel readiness probes now default to 5 seconds instead of 1 second to accommodate high-latency links, while overall readiness budgets remain unchanged. A human must accept this change to default connection behavior.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The forwarded HTTP readiness probe now defaults to 5,000 ms instead of 1,000 ms. Callers that supply probeTimeoutMs retain their value. A test covers HTTP responses delayed by 2.5 seconds.

Changes: SSH tunnel readiness

Layer / File(s) Summary
Tunnel probe timeout and test
packages/ssh/src/tunnel.ts, packages/ssh/src/tunnel.test.ts
The forwarded HTTP readiness probe now uses a 5,000 ms default unless a caller provides probeTimeoutMs. A test verifies that ensureEnvironment succeeds when HTTP responses take 2.5 seconds.

Estimated code review effort: 2 (Simple) | ~8 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested labels: bugfix

Suggested reviewers: jgratton24, t3dotgg, maria-rcks

Merge Risk: Minimal · up to 04ab9

SSH remote environments should now connect over slow, high-latency links, where the readiness probe previously timed out at 1 second. No merge-blocking risk remains.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the SSH environment connection fix, and the change raises the per-probe readiness timeout that caused failures on links with about 1s round-trip time. The title is specific and accurat…
Linked Issues check Passed Issue #7733 requires that an SSH remote environment connect when the tunnel and remote server are healthy, even if the link RTT is about 1s. The reviewed diff of packages/ssh/src/tunnel.ts adds SSH_TU…
Out of Scope Changes check Passed The whole-PR change set is small. It consists of one new constant with a comment, one changed default in waitForHttpReady (tunnel.ts), and one added test in tunnel.test.ts. The constant and default ch…
Description check Passed The description is thorough and covers the template's core sections. Problem: each SSH readiness probe through the ssh -L forward was capped at 1000ms, so at about 1s RTT every probe timed out within …
Full details: Title check

Explanation

The title names the SSH environment connection fix, and the change raises the per-probe readiness timeout that caused failures on links with about 1s round-trip time. The title is specific and accurately reflects the main change. The 'fix(ssh)' prefix follows the conventional commit style in the template.

Full details: Linked Issues check

Explanation

Issue #7733 requires that an SSH remote environment connect when the tunnel and remote server are healthy, even if the link RTT is about 1s. The reviewed diff of packages/ssh/src/tunnel.ts adds SSH_TUNNEL_READY_PROBE_TIMEOUT_MS = 5_000 and uses it as the default probeTimeoutMs in the tunnel-side waitForHttpReady wrapper. Before this change, that default was SSH_READY_PROBE_TIMEOUT_MS = 1_000, which caused the 'Backend readiness probe exceeded 1000ms' failures in the issue logs. The 20s first-connect budget is unchanged. The remote launch-script probe and the launch-lock arithmetic still use the 1s constant, as the PR description states. The added test in packages/ssh/src/tunnel.test.ts covers HTTP responses taking 2.5s through the tunnel. I confirmed the code change in the diff. I did not run the tests. The test and check results come from the PR description and are not independently verified here. The PR does not cover the 2s existing-tunnel check or the remote launch probe, which the issue does not require to change. The 'Evidence' section is non-coding and does not affect this result.

Full details: Out of Scope Changes check

Explanation

The whole-PR change set is small. It consists of one new constant with a comment, one changed default in waitForHttpReady (tunnel.ts), and one added test in tunnel.test.ts. The constant and default change are directly tied to the readiness probe timeout in #7733. The test supports that objective. The shared httpReadiness default and the remote launch-script probe are not changed, which keeps the change within the issue scope. The PR description discloses the remaining 2s existing-tunnel check limitation and an unrelated failing test that also fails on the base commit. These are not changes introduced by this PR.

Full details: Description check

Explanation

The description is thorough and covers the template's core sections. Problem: each SSH readiness probe through the ssh -L forward was capped at 1000ms, so at about 1s RTT every probe timed out within the 20s budget and the environment never connected. Change: the tunnel-side waitForHttpReady wrapper now defaults to a new SSH_TUNNEL_READY_PROBE_TIMEOUT_MS of 5000, with the overall budgets and the remote launch-script probe left unchanged. Scope: it describes a bug fix and links issue #7733 that shows the bug. Verification: it gives a before/after reproduction on a real high-latency SSH host, screenshots and recordings, a focused unit test that fails before and passes after, and a full check:changed run. It also states remaining limitations and the one failing unrelated test. The template asks for the agent model and harness if an agent was used; the description says 'AI-assisted' but does not name the model and harness, which is a minor gap. The Verification section is strong, and the Evidence section links to external artifacts that reviewers cannot verify from the repository alone.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between a909ea4 and 04ab913.

Files selected for processing: 2

  • packages/ssh/src/tunnel.test.ts
  • packages/ssh/src/tunnel.ts

Limit details: You’ve used all 10 included reviews currently available.

@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Other scenarios

All 64 scenario measurements match main.

All 64 scenario measurements
Provider Scenario Metric Main baseline This PR Impact PR ceiling
Codex short turn resume WebSocket decoded 10.5 KiB 10.5 KiB 0 B (0.0%) 12.7 KiB ✅
Codex short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn running, cold open HTTP decoded 281.7 KiB 281.7 KiB 0 B (0.0%) 302.7 KiB ✅
Codex long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Codex long turn running, history page HTTP decoded 92.4 KiB 92.4 KiB 0 B (0.0%) 107.4 KiB ✅
Codex long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Codex long turn finished, cold open HTTP decoded 381.5 KiB 381.5 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, socket cold open WebSocket decoded 381.6 KiB 381.6 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, work page HTTP decoded 186.2 KiB 186.2 KiB 0 B (0.0%) 200.2 KiB ✅
Codex long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Codex idle thread resume WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell snapshot HTTP decoded 41.6 KiB 41.6 KiB 0 B (0.0%) 53.7 KiB ✅
Codex fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Codex fleet shell subscribe WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Codex fleet shell resume check rows 0 0 0 0 ✅
Codex stale shell reload HTTP decoded 41.6 KiB 41.6 KiB 0 B (0.0%) 53.7 KiB ✅
Codex stale shell reload rows 27 27 0 (0.0%) 27 ✅
Codex stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Codex stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex stale todo list cold open HTTP decoded 72.4 KiB 72.4 KiB 0 B (0.0%) 83.0 KiB ✅
Codex stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Codex stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅
Claude short turn resume WebSocket decoded 10.6 KiB 10.6 KiB 0 B (0.0%) 12.7 KiB ✅
Claude short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn running, cold open HTTP decoded 283.3 KiB 283.3 KiB 0 B (0.0%) 302.7 KiB ✅
Claude long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Claude long turn running, history page HTTP decoded 93.4 KiB 93.4 KiB 0 B (0.0%) 107.4 KiB ✅
Claude long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Claude long turn finished, cold open HTTP decoded 384.2 KiB 384.2 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, socket cold open WebSocket decoded 384.3 KiB 384.3 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, work page HTTP decoded 188.1 KiB 188.1 KiB 0 B (0.0%) 200.2 KiB ✅
Claude long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Claude idle thread resume WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell snapshot HTTP decoded 42.2 KiB 42.2 KiB 0 B (0.0%) 53.7 KiB ✅
Claude fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Claude fleet shell subscribe WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Claude fleet shell resume check rows 0 0 0 0 ✅
Claude stale shell reload HTTP decoded 42.2 KiB 42.2 KiB 0 B (0.0%) 53.7 KiB ✅
Claude stale shell reload rows 27 27 0 (0.0%) 27 ✅
Claude stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Claude stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude stale todo list cold open HTTP decoded 73.4 KiB 73.4 KiB 0 B (0.0%) 83.0 KiB ✅
Claude stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Claude stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅

Baseline: a909ea4 · PR result: 04ab913 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@juliusmarminge
juliusmarminge merged commit c58450f into main Nov 12, 2026
36 checks passed
@juliusmarminge
juliusmarminge deleted the factory/issue-7733-a7815 branch November 12, 2026 18:02
Pipyakas pushed a commit to Pipyakas/t3code that referenced this pull request Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS 0-9 changed lines (additions + deletions). vouch:trusted Author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: SSH remote environment never connects when link RTT exceeds ~1s, eg on in-flight wifi (readiness probe deadline is 1000ms)

1 participant